﻿using System;
using System.Globalization;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Security.Principal;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.Security;
using NRails.Dinner.Models;

namespace NRails.Dinner.Controllers
{
    [HandleError]
    public class AccountController : Controller
    {
        public this()
        {
            this(null, null)
        }

        public this(formsAuth : IFormsAuthentication, service : IMembershipService)
        {
            FormsAuth = formsAuth ?? FormsAuthenticationService();
            MembershipService = service ?? AccountMembershipService();
        }

        public FormsAuth : IFormsAuthentication { get; private set; }

        public MembershipService : IMembershipService { get; private set; }

        public LogOn() : ActionResult
        {
            View()
        }

        [AcceptVerbs(HttpVerbs.Post)]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBestrings",
            Justification = "Needs to take same parameter type as Controller.Redirect()")]
        public LogOn(userName : string, password : string, rememberMe : bool, returnUrl : string) : ActionResult
        {
            if (!ValidateLogOn(userName, password))
            {
                ViewData["rememberMe"] = rememberMe;
                View()
            }
            else
            {
                FormsAuth.SignIn(userName, rememberMe);
                if (!string.IsNullOrEmpty(returnUrl))
                    Redirect(returnUrl)
                else
                    RedirectToAction("Index", "Home")
            }
        }

        public LogOff() : ActionResult
        {
            FormsAuth.SignOut();
            RedirectToAction("Index", "Home")
        }

        public Register() : ActionResult
        {
            ViewData["PasswordLength"] = MembershipService.MinPasswordLength;
            View()
        }

        [AcceptVerbs(HttpVerbs.Post)]
        public Register(userName : string, email : string, password : string, confirmPassword : string) : ActionResult
        {
            ViewData["PasswordLength"] = MembershipService.MinPasswordLength;

            if (ValidateRegistration(userName, email, password, confirmPassword))
            {
                // Attempt to register the user
                def createStatus = MembershipService.CreateUser(userName, password, email);

                if (createStatus == MembershipCreateStatus.Success)
                {
                    FormsAuth.SignIn(userName, false /* createPersistentCookie */);
                    RedirectToAction("Index", "Home")
                }
                else
                {
                    ModelState.AddModelError("_FORM", ErrorCodeTostring(createStatus));
                    // If we got this far, something failed, redisplay form
                    View()
                }
            }
            else
                // If we got this far, something failed, redisplay form
                View()
        }

        [Authorize]
        public ChangePassword() : ActionResult
        {
            ViewData["PasswordLength"] = MembershipService.MinPasswordLength;
            View()
        }

        [Authorize]
        [AcceptVerbs(HttpVerbs.Post)]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
            Justification = "Exceptions result in password not being changed.")]
        public ChangePassword(currentPassword : string, newPassword : string, confirmPassword : string) : ActionResult
        {
            ViewData["PasswordLength"] = MembershipService.MinPasswordLength;

            if (!ValidateChangePassword(currentPassword, newPassword, confirmPassword))
            {
                View()
            }
            else
            {
                try
                {
                    if (MembershipService.ChangePassword(User.Identity.Name, currentPassword, newPassword))
                    {
                        RedirectToAction("ChangePasswordSuccess")
                    }
                    else
                    {
                        ModelState.AddModelError("_FORM", "The current password is incorrect or the new password is invalid.");
                        View()
                    }
                }
                catch
                {
                    | _ =>
                        ModelState.AddModelError("_FORM", "The current password is incorrect or the new password is invalid.");
                        View()
                }
            }
        }

        public ChangePasswordSuccess() : ActionResult
        {
            View();
        }

        protected override OnActionExecuting(filterContext : ActionExecutingContext) : void
        {
            when (filterContext.HttpContext.User.Identity is WindowsIdentity)
            {
                throw InvalidOperationException("Windows authentication is not supported.")
            }
        }

        #region Validation Methods

        private ValidateChangePassword(currentPassword : string, newPassword : string, confirmPassword : string) : bool
        {
            when (string.IsNullOrEmpty(currentPassword))
            {
                ModelState.AddModelError("currentPassword", "You must specify a current password.");
            }
            when (newPassword == null || newPassword.Length < MembershipService.MinPasswordLength)
            {
                ModelState.AddModelError("newPassword",
                    string.Format(CultureInfo.CurrentCulture,
                         "You must specify a new password of {0} or more characters.",
                         MembershipService.MinPasswordLength));
            }

            when (!string.Equals(newPassword, confirmPassword, StringComparison.Ordinal))
            {
                ModelState.AddModelError("_FORM", "The new password and confirmation password do not match.");
            }

            ModelState.IsValid
        }

        private ValidateLogOn(userName : string, password : string) : bool
        {
            when (string.IsNullOrEmpty(userName))
            {
                ModelState.AddModelError("username", "You must specify a username.");
            }
            when (string.IsNullOrEmpty(password))
            {
                ModelState.AddModelError("password", "You must specify a password.");
            }
            when (!MembershipService.ValidateUser(userName, password))
            {
                ModelState.AddModelError("_FORM", "The username or password provided is incorrect.");
            }

            ModelState.IsValid
        }

        private ValidateRegistration(userName : string, email : string, password : string, confirmPassword : string) : bool
        {
            when (string.IsNullOrEmpty(userName))
            {
                ModelState.AddModelError("username", "You must specify a username.");
            }
            when (string.IsNullOrEmpty(email))
            {
                ModelState.AddModelError("email", "You must specify an email address.");
            }
            when (password == null || password.Length < MembershipService.MinPasswordLength) {
                ModelState.AddModelError("password",
                    string.Format(CultureInfo.CurrentCulture,
                         "You must specify a password of {0} or more characters.",
                         MembershipService.MinPasswordLength));
            }
            when (!string.Equals(password, confirmPassword, StringComparison.Ordinal))
            {
                ModelState.AddModelError("_FORM", "The new password and confirmation password do not match.");
            }
            ModelState.IsValid
        }

        private static ErrorCodeTostring(createStatus : MembershipCreateStatus) : string
        {
            // See http://msdn.microsoft.com/en-us/library/system.web.security.membershipcreatestatus.aspx for
            // a full list of status codes.
            match (createStatus)
            {
                | MembershipCreateStatus.DuplicateUserName =>
                    "Username already exists. Please enter a different user name.";
                | MembershipCreateStatus.DuplicateEmail =>
                    "A username for that e-mail address already exists. Please enter a different e-mail address.";
                | MembershipCreateStatus.InvalidPassword =>
                    "The password provided is invalid. Please enter a valid password value.";
                | MembershipCreateStatus.InvalidEmail =>
                     "The e-mail address provided is invalid. Please check the value and try again.";
                | MembershipCreateStatus.InvalidAnswer =>
                     "The password retrieval answer provided is invalid. Please check the value and try again.";
                | MembershipCreateStatus.InvalidQuestion =>
                     "The password retrieval question provided is invalid. Please check the value and try again.";
                | MembershipCreateStatus.InvalidUserName =>
                     "The user name provided is invalid. Please check the value and try again.";
                | MembershipCreateStatus.ProviderError =>
                     "The authentication provider ed an error. Please verify your entry and try again. If the problem persists, please contact your system administrator.";
                | MembershipCreateStatus.UserRejected =>
                     "The user creation request has been canceled. Please verify your entry and try again. If the problem persists, please contact your system administrator.";
                | _ =>
                    "An unknown error occurred. Please verify your entry and try again. If the problem persists, please contact your system administrator.";
            }
        }
        #endregion
    }

    // The FormsAuthentication type is sealed and contains static members, so it is difficult to
    // unit test code that calls its members. The interface and helper class below demonstrate
    // how to create an abstract wrapper around such a type in order to make the AccountController
    // code unit testable.

    public interface IFormsAuthentication
    {
        SignIn(userName : string, createPersistentCookie : bool) : void;
        SignOut() : void;
    }

    public class FormsAuthenticationService : IFormsAuthentication
    {
        public SignIn(userName : string, createPersistentCookie : bool) : void
        {
            FormsAuthentication.SetAuthCookie(userName, createPersistentCookie);
        }
        public SignOut() : void
        {
            FormsAuthentication.SignOut();
        }
    }

    public interface IMembershipService
    {
        MinPasswordLength : int { get; }

        ValidateUser(userName : string, password : string) : bool;
        CreateUser(userName : string, password : string, email : string) : MembershipCreateStatus;
        ChangePassword(userName : string, oldPassword : string, newPassword : string) : bool;
    }

    public class AccountMembershipService : NRails.Dinner.Controllers.IMembershipService
    {
        _provider : MembershipProvider;

        public this()
        {
            this(null)
        }

        public this(provider : MembershipProvider)
        {
            _provider = provider ?? Membership.Provider
        }

        public MinPasswordLength : int
        {
            get
            {
                _provider.MinRequiredPasswordLength
            }
        }

        public ValidateUser(userName : string, password : string) : bool
        {
            _provider.ValidateUser(userName, password)
        }

        public CreateUser(userName : string, password : string, email : string) : MembershipCreateStatus
        {
            mutable status;
            _ = _provider.CreateUser(userName, password, email, null, null, true, null, out status);
            status;
        }

        public ChangePassword(userName : string, oldPassword : string, newPassword : string) : bool
        {
            def currentUser = _provider.GetUser(userName, true /* userIsOnline */);
            currentUser.ChangePassword(oldPassword, newPassword)
        }
    }
}
